/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership. The ASF licenses this file to You under the Apache License, Version
 * 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
 * http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
 * the specific language governing permissions and limitations under the License.
 */

package jmine.tec.test.junit.runner;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import jmine.tec.test.junit.runner.formatter.JUnitResultFormatter;
import jmine.tec.test.junit.runner.formatter.TestOutputType;
import junit.framework.AssertionFailedError;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestFailure;
import junit.framework.TestListener;
import junit.framework.TestResult;
import junit.framework.TestSuite;

import org.apache.tools.ant.util.FileUtils;
import org.apache.tools.ant.util.StringUtils;
import org.apache.tools.ant.util.TeeOutputStream;

/**
 * Simple Testrunner for JUnit that runs all tests of a testsuite.
 * <p>
 * This TestRunner expects a name of a TestCase class as its argument. If this class provides a static suite() method it will be called and
 * the resulting Test will be run. So, the signature should be
 *
 * <pre>
 * <code>
 *     public static junit.framework.Test suite()
 * </code>
 * </pre>
 * <p>
 * If no such method exists, all public methods starting with "test" and taking no argument will be run.
 * <p>
 * Summary output is generated at the end.
 */
public class ConsoleTestRunner implements TestListener {

    /**
     * Holds the registered formatters.
     */
    private Vector<JUnitResultFormatter> formatters = new Vector<JUnitResultFormatter>();

    private static List<FormatterElement> testOutputFormatter = new ArrayList<FormatterElement>();

    /**
     * Collects TestResults.
     */
    private TestResult res;

    /**
     * Do we filter junit.*.* stack frames out of failure and error exceptions.
     */
    private static boolean filtertrace = true;

    private static final String JUNIT_4_TEST_ADAPTER = "junit.framework.JUnit4TestAdapter";

    private static final String[] DEFAULT_TRACE_FILTERS = new String[]{ "junit.framework.TestCase", "junit.framework.TestResult",
            "junit.framework.TestSuite",
            "junit.framework.Assert.", // don't filter AssertionFailure
            "junit.swingui.TestRunner", "junit.awtui.TestRunner", "junit.textui.TestRunner", "java.lang.reflect.Method.invoke(",
            "sun.reflect.", "org.apache.tools.ant.",
            // JUnit 4 support:
            "org.junit.", "junit.framework.JUnit4TestAdapter", " more", };

    private static final String TESTLISTENER_PREFIX = "junit.framework.TestListener: ";

    /**
     * No problems with this test.
     */
    private static final int SUCCESS = 0;

    /**
     * Some tests failed.
     */
    private static final int FAILURES = 1;

    /**
     * An error occurred.
     */
    private static final int ERRORS = 2;

    /**
     * Do we stop on errors.
     */
    private boolean haltOnError = false;

    /**
     * Do we stop on test failures.
     */
    private boolean haltOnFailure = false;

    /**
     * Returncode
     */
    private int retCode = SUCCESS;

    /**
     * The TestSuite we are currently running.
     */
    private JUnitTest junitTest;

    /** output written during the test */
    private PrintStream systemError;

    /** Error output during the test */
    private PrintStream systemOut;

    /** Do we print TestListener events? */
    private boolean logTestListenerEvents = false;

    /** Turned on if we are using JUnit 4 for this test suite. see #38811 */
    private boolean junit4;

    /** Names of test methods to execute */
    private String[] methods = null;

    private PrintStream savedOut = null;

    private PrintStream savedErr = null;

    /**
     * Constructor to use when the user has specified a classpath.
     *
     * @param test the test to run.
     * @param methods methods
     * @param haltOnError whether to stop the run if an error is found.
     * @param filtertrace whether to filter junit.*.* stack frames out of exceptions
     * @param haltOnFailure whether to stop the run if failure is found.
     * @param logTestListenerEvents whether to print TestListener events.
     * @since 1.8.2
     */
    public ConsoleTestRunner(JUnitTest test, String[] methods, boolean haltOnError, boolean filtertrace, boolean haltOnFailure,
            boolean logTestListenerEvents) {
        ConsoleTestRunner.filtertrace = filtertrace; // XXX clumsy, should use instance field somehow
        this.junitTest = test;
        this.haltOnError = haltOnError;
        this.haltOnFailure = haltOnFailure;
        this.logTestListenerEvents = logTestListenerEvents;
        this.methods = methods != null ? (String[]) methods.clone() : null;
    }

    /**
     * Set-up IOStreams.
     *
     * @param o output
     * @param e error
     */
    private void setupIOStreams(ByteArrayOutputStream o, ByteArrayOutputStream e) {
        this.systemOut = new PrintStream(o);
        this.systemError = new PrintStream(e);

        this.savedOut = System.out;
        this.savedErr = System.err;

        System.setOut(new PrintStream(new TeeOutputStream(this.savedOut, this.systemOut)));
        System.setErr(new PrintStream(new TeeOutputStream(this.savedErr, this.systemError)));
    }

    /**
     * Run the test.
     */
    public void run() {
        this.res = new TestResult();
        this.res.addListener(this.wrapListener(this));
        final int size = this.formatters.size();
        for (int i = 0; i < size; i++) {
            this.res.addListener(this.wrapListener(this.formatters.elementAt(i)));
        }

        ByteArrayOutputStream errStrm = new ByteArrayOutputStream();
        ByteArrayOutputStream outStrm = new ByteArrayOutputStream();
        this.setupIOStreams(outStrm, errStrm);

        Test suite = null;
        Throwable exception = null;

        try {
            try {
                suite = this.checkJunitVersion();
            } catch (Throwable e) {
                this.retCode = ERRORS;
                exception = e;
            }

            long start = System.currentTimeMillis();
            this.fireStartTestSuite();
            if (exception != null) { // had an exception constructing suite
                final int formatterSize = this.formatters.size();
                for (int i = 0; i < formatterSize; i++) {
                    ((TestListener) this.formatters.elementAt(i)).addError(null, exception);
                }
                this.junitTest.setCounts(1, 0, 1);
                this.junitTest.setRunTime(0);
            } else {
                try {
                    this.logTestListenerEvent("tests to run: " + suite.countTestCases());
                    suite.run(this.res);
                } finally {
                    if (this.junit4 || suite.getClass().getName().equals(JUNIT_4_TEST_ADAPTER)) {
                        int[] cnts = this.findJUnit4FailureErrorCount(this.res);
                        this.junitTest.setCounts(this.res.runCount(), cnts[0], cnts[1]);
                    } else {
                        this.junitTest.setCounts(this.res.runCount(), this.res.failureCount(), this.res.errorCount());
                    }
                    this.junitTest.setRunTime(System.currentTimeMillis() - start);
                }
            }
        } finally {
            if (this.savedOut != null) {
                System.setOut(this.savedOut);
            }
            if (this.savedErr != null) {
                System.setErr(this.savedErr);
            }

            this.systemError.close();
            this.systemError = null;
            this.systemOut.close();
            this.systemOut = null;
        }
        this.fireEndTestSuite();

        // junitTest has the correct counts for JUnit4, while res doesn't
        if (this.retCode != SUCCESS || this.junitTest.errorCount() != 0) {
            this.retCode = ERRORS;
        } else if (this.junitTest.failureCount() != 0) {
            this.retCode = FAILURES;
        }
    }

    /**
     * @return Test
     * @throws Exception Se ocorrer algum erro ao identificar qual é a versão do JUnit.
     */
    @SuppressWarnings("unchecked")
    private Test checkJunitVersion() throws Exception {
        Test suite = null;

        Class<? extends TestCase> testClass = (Class<? extends TestCase>) Class.forName(this.junitTest.getName());
        final boolean testMethodsSpecified = (this.methods != null);

        // check for a static suite method first, even when using JUnit 4
        Method suiteMethod = null;
        if (!testMethodsSpecified) {
            try {
                // check if there is a suite method
                suiteMethod = testClass.getMethod("suite", new Class[0]);
            } catch (NoSuchMethodException e) {
                // no appropriate suite method found. We don't report any error here since it might be perfectly normal.
            }
        }

        if (suiteMethod != null) {
            // if there is a suite method available, then tryto extract the suite from it. If there is an error here it will be
            // caught below and reported.
            suite = (Test) suiteMethod.invoke(null, new Object[0]);
        } else {
            Class junit4TestAdapterClass = null;
            boolean useSingleMethodAdapter = false;

            if (junit.framework.TestCase.class.isAssignableFrom(testClass)) {
                // Do not use JUnit 4 API for running JUnit 3.x tests - it is not able to run individual test methods.
                //
                // Technical details:
                // org.junit.runner.Request.method(Class, String).getRunner() would return a runner which always executes all test
                // methods. The reason is that the Runner would be an instance of class
                // org.junit.internal.runners.OldTestClassRunner that does not implement interface Filterable - so it is unable to
                // filter out test methods not matching the requested name.
            } else {
                // Check for JDK 5 first. Will *not* help on JDK 1.4 if only junit-4.0.jar in CP because in that case linkage of
                // whole task will already have failed! But will help if CP has junit-3.8.2.jar:junit-4.0.jar.
                // In that case first C.fN will fail with CNFE and we will avoid UnsupportedClassVersionError.

                try {
                    Class.forName("java.lang.annotation.Annotation");
                    junit4TestAdapterClass = Class.forName(JUNIT_4_TEST_ADAPTER);
                    if (testMethodsSpecified) {
                        /*
                         * We cannot try to load the JUnit4TestAdapter before trying to load JUnit4TestMethodAdapter because it might fail
                         * with NoClassDefFoundException, instead of plain ClassNotFoundException.
                         */
                        junit4TestAdapterClass = Class.forName("org.apache.tools.ant.taskdefs.optional.junit.JUnit4TestMethodAdapter");
                        useSingleMethodAdapter = true;
                    }
                } catch (ClassNotFoundException e) {
                    // OK, fall back to JUnit 3.
                }
            }

            this.junit4 = junit4TestAdapterClass != null;
            if (this.junit4) {
                // Let's use it!
                Class[] formalParams;
                Object[] actualParams;
                if (useSingleMethodAdapter) {
                    formalParams = new Class[]{ Class.class, String[].class };
                    actualParams = new Object[]{ testClass, this.methods };
                } else {
                    formalParams = new Class[]{ Class.class };
                    actualParams = new Object[]{ testClass };
                }
                suite = (Test) junit4TestAdapterClass.getConstructor(formalParams).newInstance(actualParams);
            } else {
                // Use JUnit 3.

                // try to extract a test suite automatically this
                // will generate warnings if the class is no
                // suitable Test
                if (!testMethodsSpecified) {
                    suite = new TestSuite(testClass);
                } else if (this.methods.length == 1) {
                    suite = TestSuite.createTest(testClass, this.methods[0]);
                } else {
                    TestSuite testSuite = new TestSuite(testClass.getName());
                    for (int i = 0; i < this.methods.length; i++) {
                        testSuite.addTest(TestSuite.createTest(testClass, this.methods[i]));
                    }
                    suite = testSuite;
                }
            }

        }

        return suite;
    }

    /**
     * Returns what System.exit() would return in the standalone version.
     *
     * @return 2 if errors occurred, 1 if tests failed else 0.
     */
    public int getRetCode() {
        return this.retCode;
    }

    /**
     * Interface TestListener.
     * <p>
     * A new Test is started.
     *
     * @param t the test.
     */
    public void startTest(Test t) {
        String testName = JUnitVersionHelper.getTestCaseName(t);
        this.logTestListenerEvent("startTest(" + testName + ")");
    }

    /**
     * Interface TestListener.
     * <p>
     * A Test is finished.
     *
     * @param test the test.
     */
    public void endTest(Test test) {
        String testName = JUnitVersionHelper.getTestCaseName(test);
        this.logTestListenerEvent("endTest(" + testName + ")");
    }

    /**
     * Log event
     *
     * @param msg message
     */
    private void logTestListenerEvent(String msg) {
        if (this.logTestListenerEvents) {
            PrintStream out = this.savedOut != null ? this.savedOut : System.out;
            out.flush();
            if (msg == null) {
                out.println(TESTLISTENER_PREFIX + "null");
            } else {
                StringTokenizer msgLines = new StringTokenizer(msg, "\r\n", false);
                while (msgLines.hasMoreTokens()) {
                    out.println(TESTLISTENER_PREFIX + msgLines.nextToken());
                }
            }
            out.flush();
        }
    }

    /**
     * Interface TestListener for JUnit &lt;= 3.4.
     * <p>
     * A Test failed.
     *
     * @param test the test.
     * @param t the exception thrown by the test.
     */
    public void addFailure(Test test, Throwable t) {
        String testName = JUnitVersionHelper.getTestCaseName(test);
        this.logTestListenerEvent("addFailure(" + testName + ", " + t.getMessage() + ")");
        if (this.haltOnFailure) {
            this.res.stop();
        }
    }

    /**
     * Interface TestListener for JUnit &gt; 3.4.
     * <p>
     * A Test failed.
     *
     * @param test the test.
     * @param t the assertion thrown by the test.
     */
    public void addFailure(Test test, AssertionFailedError t) {
        this.addFailure(test, (Throwable) t);
    }

    /**
     * Interface TestListener.
     * <p>
     * An error occurred while running the test.
     *
     * @param test the test.
     * @param t the error thrown by the test.
     */
    public void addError(Test test, Throwable t) {
        String testName = JUnitVersionHelper.getTestCaseName(test);
        this.logTestListenerEvent("addError(" + testName + ", " + t.getMessage() + ")");
        if (this.haltOnError) {
            this.res.stop();
        }
    }

    /**
     * Handle a string destined for standard output.
     *
     * @param output the string to output
     */
    public void handleOutput(String output) {
        if (!this.logTestListenerEvents && output.startsWith(TESTLISTENER_PREFIX)) {
            // ignore
        } else if (this.systemOut != null) {
            this.systemOut.print(output);
        }
    }

    /**
     * Handle input.
     *
     * @param buffer not used.
     * @param offset not used.
     * @param length not used.
     * @return -1 always.
     * @throws IOException never.
     * @see org.apache.tools.ant.Task#handleInput(byte[], int, int)
     * @since Ant 1.6
     */
    public int handleInput(byte[] buffer, int offset, int length) throws IOException {
        return -1;
    }

    /** {@inheritDoc}. */
    public void handleErrorOutput(String output) {
        if (this.systemError != null) {
            this.systemError.print(output);
        }
    }

    /** {@inheritDoc}. */
    public void handleFlush(String output) {
        if (this.systemOut != null) {
            this.systemOut.print(output);
        }
    }

    /** {@inheritDoc}. */
    public void handleErrorFlush(String output) {
        if (this.systemError != null) {
            this.systemError.print(output);
        }
    }

    /**
     * Trigger when a test suite starts
     */
    private void fireStartTestSuite() {
        final int size = this.formatters.size();
        for (int i = 0; i < size; i++) {
            this.formatters.elementAt(i).startTestSuite(this.junitTest);
        }
    }

    /**
     * Trigger when a test suite ends
     */
    private void fireEndTestSuite() {
        final int size = this.formatters.size();
        for (int i = 0; i < size; i++) {
            this.formatters.elementAt(i).endTestSuite(this.junitTest);
        }
    }

    /**
     * Add a formatter.
     *
     * @param f the formatter to add.
     */
    public void addFormatter(JUnitResultFormatter f) {
        this.formatters.addElement(f);
    }

    /**
     * Entry point for standalone (forked) mode. Parameters: testcaseclassname plus parameters in the format key=value, none of which is
     * required.
     * <table cols="4" border="1">
     * <tr>
     * <th>key</th>
     * <th>description</th>
     * <th>default value</th>
     * </tr>
     * <tr>
     * <td>haltOnError</td>
     * <td>halt test on errors?</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>haltOnFailure</td>
     * <td>halt test on failures?</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>formatter</td>
     * <td>A JUnitResultFormatter given as classname,filename. If filename is ommitted, System.out is assumed.</td>
     * <td>none</td>
     * </tr>
     * <tr>
     * <td>showoutput</td>
     * <td>send output to System.err/.out as well as to the formatters?</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>logtestlistenerevents</td>
     * <td>log TestListener events to System.out.</td>
     * <td>false</td>
     * </tr>
     * <tr>
     * <td>methods</td>
     * <td>Comma-separated list of names of individual test methods to execute.</td>
     * <td>null</td>
     * </tr>
     * </table>
     *
     * @param args the command line arguments.
     * @throws IOException on error.
     */
    public static void main(String[] args) throws IOException {
        String[] methods = null;
        boolean haltError = false;
        boolean haltFail = false;
        boolean stackfilter = true;
        Properties props = new Properties();
        boolean showOut = true;
        boolean outputToFormat = true;
        boolean logTestListenerEvents = true;

        if (args.length == 0) {
            System.err.println("required argument TestClassName missing");
            System.exit(ERRORS);
        }

        testOutputFormatter.add(new FormatterElement(TestOutputType.XML));
        String testname = args[0];
        JUnitTest t = new JUnitTest(testname);
        t.setOutfile(testname);
        t.setProperties(props);
        int returnCode = launch(t, methods, haltError, stackfilter, haltFail, showOut, outputToFormat, logTestListenerEvents);

        if (returnCode != 0) {
            System.err.println("Test exit code: " + returnCode);
        }

        // rafael.volpato: sair com exit code 0, para que o jenkins não fique com o status "BROKEN" e sim "INSTABLE".
        System.exit(0);
    }

    /**
     * Copy formatters to runner.
     *
     * @param runner runner
     * @param test test
     */
    private static void transferFormatters(ConsoleTestRunner runner, JUnitTest test) {
        for (FormatterElement formatterElement : testOutputFormatter) {
            if (formatterElement.getUseFile()) {
                File destFile = new File(test.getTodir(), test.getOutfile() + formatterElement.getOutputType().getFileExtension());
                formatterElement.setOutfile(destFile);
            }
            runner.addFormatter(formatterElement.createFormatter());
        }
    }

    /**
     * Returns a filtered stack trace. This is ripped out of junit.runner.BaseTestRunner.
     *
     * @param t the exception to filter.
     * @return the filtered stack trace.
     */
    public static String getFilteredTrace(Throwable t) {
        String trace = StringUtils.getStackTrace(t);
        return ConsoleTestRunner.filterStack(trace);
    }

    /**
     * Filters stack frames from internal JUnit and Ant classes
     *
     * @param stack the stack trace to filter.
     * @return the filtered stack.
     */
    public static String filterStack(String stack) {
        if (!filtertrace) {
            return stack;
        }
        StringWriter sw = new StringWriter();
        BufferedWriter pw = new BufferedWriter(sw);
        StringReader sr = new StringReader(stack);
        BufferedReader br = new BufferedReader(sr);

        String line;
        try {
            while ((line = br.readLine()) != null) {
                if (!filterLine(line)) {
                    pw.write(line);
                    pw.newLine();
                }
            }
        } catch (Exception e) {
            return stack; // return the stack unfiltered
        } finally {
            FileUtils.close(pw);
        }
        return sw.toString();
    }

    /**
     * Check if line should be added to stacktrace (ignore junit lines from stack).
     *
     * @param line line
     * @return <code>true</code> if line should be included, <code>false</code> otherwise
     */
    private static boolean filterLine(String line) {
        for (int i = 0; i < DEFAULT_TRACE_FILTERS.length; i++) {
            if (line.indexOf(DEFAULT_TRACE_FILTERS[i]) != -1) {
                return true;
            }
        }
        return false;
    }

    /**
     * Launch test.
     *
     * @param t {@link JUnitTest}
     * @param methods methods
     * @param haltError halt on error
     * @param stackfilter filter stack trace
     * @param haltFail halt on failure
     * @param showOut show output
     * @param outputToFormat output to format
     * @param logTestListenerEvents log test listener events.
     * @return test execution return code.
     */
    private static int launch(JUnitTest t, String[] methods, boolean haltError, boolean stackfilter, boolean haltFail, boolean showOut,
            boolean outputToFormat, boolean logTestListenerEvents) {
        ConsoleTestRunner runner = new ConsoleTestRunner(t, methods, haltError, stackfilter, haltFail, logTestListenerEvents);
        transferFormatters(runner, t);
        runner.run();
        return runner.getRetCode();
    }

    /**
     * Modifies a TestListener when running JUnit 4: treats AssertionFailedError as a failure not an error.
     *
     * @param testListener {@link TestListener}
     * @return wrap'd {@link TestListener}
     */
    private TestListener wrapListener(final TestListener testListener) {
        return new TestListener() {
            public void addError(Test test, Throwable t) {
                if (ConsoleTestRunner.this.junit4 && t instanceof AssertionFailedError) {
                    // JUnit 4 does not distinguish between errors and failures
                    // even in the JUnit 3 adapter.
                    // So we need to help it a bit to retain compatibility for JUnit 3 tests.
                    testListener.addFailure(test, (AssertionFailedError) t);
                } else if (ConsoleTestRunner.this.junit4 && t instanceof AssertionError) {
                    // Not strictly necessary but probably desirable.
                    // JUnit 4-specific test GUIs will show just "failures".
                    // But Ant's output shows "failures" vs. "errors".
                    // We would prefer to show "failure" for things that logically are.
                    String msg = t.getMessage();
                    AssertionFailedError failure = msg != null ? new AssertionFailedError(msg) : new AssertionFailedError();
                    failure.setStackTrace(t.getStackTrace());
                    testListener.addFailure(test, failure);
                } else {
                    testListener.addError(test, t);
                }
            }

            public void addFailure(Test test, AssertionFailedError t) {
                testListener.addFailure(test, t);
            }

            public void endTest(Test test) {
                testListener.endTest(test);
            }

            public void startTest(Test test) {
                testListener.startTest(test);
            }
        };
    }

    /**
     * Use instead of TestResult.get{Failure,Error}Count on JUnit 4, since the adapter claims that all failures are errors.
     *
     * @param result {@link TestResult}
     * @return numer of failures and errors
     */
    private int[] findJUnit4FailureErrorCount(TestResult result) {
        int failures = 0;
        int errors = 0;
        Enumeration e = result.failures();
        while (e.hasMoreElements()) {
            e.nextElement();
            failures++;
        }
        e = result.errors();
        while (e.hasMoreElements()) {
            Throwable t = ((TestFailure) e.nextElement()).thrownException();
            if (t instanceof AssertionFailedError || t instanceof AssertionError) {
                failures++;
            } else {
                errors++;
            }
        }
        return new int[]{ failures, errors };
    }

}
